﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Xml;

namespace SCSInternetBanking.LibertyReserve
{
    public sealed class JsonApiAgent : AbstractApiAgent
    {
        #region Json API Request Contract

        [DataContract]
        private abstract class BaseRequest
        {
            [DataMember]
            public string Id { get; set; }

            [DataMember]
            public string Account { get; set; }

            [DataMember]
            public string Api { get; set; }

            [DataMember]
            public string Token { get; set; }
        }

        [DataContract]
        private class AccountNameRequest : BaseRequest
        {
            [DataMember]
            public string Search { get; set; }
        }

        [DataContract]
        private class BalanceRequest : BaseRequest
        {
        }

        [DataContract]
        private class FindTransactionRequest : BaseRequest
        {
            [DataMember]
            public long Batch { get; set; }
        }

        [DataContract]
        private class HistoryRequest : BaseRequest
        {
            [DataMember]
            public string From { get; set; }

            [DataMember]
            public string Till { get; set; }

            [DataMember]
            public Currency? Currency { get; set; }

            [DataMember]
            public SearchDirection? Direction { get; set; }

            [DataMember]
            public string RelatedAccount { get; set; }

            [DataMember]
            public string Reference { get; set; }

            [DataMember]
            public string Type { get; set; }

            [DataMember]
            public Source? Source { get; set; }

            [DataMember]
            public SearchPrivate? Private { get; set; }

            [DataMember]
            public decimal? AmountFrom { get; set; }

            [DataMember]
            public decimal? AmountTo { get; set; }

            [DataMember]
            public int? Page { get; set; }

            [DataMember]
            public int? Size { get; set; }
        }

        [DataContract]
        private class TransferRequest : BaseRequest
        {
            [DataMember]
            public string Reference { get; set; }

            [DataMember]
            public string Type { get; set; }

            [DataMember]
            public string Payee { get; set; }

            [DataMember]
            public Currency Currency { get; set; }

            [DataMember]
            public decimal Amount { get; set; }

            [DataMember]
            public string Memo { get; set; }

            [DataMember]
            public bool? Private { get; set; }

            [DataMember]
            public PaymentPurpose Purpose { get; set; }
        }

        #endregion

        #region Json API Response Contract

        private enum Status
        {
            None = 0,

            Success,

            Error
        }

        [DataContract]
        private class Error
        {
            [DataMember]
            public int ErrorCode { get; set; }

            [DataMember]
            public string ErrorMessage { get; set; }
        }

        [DataContract]
        private abstract class BaseResponse
        {
            [DataMember]
            public string Id { get; set; }

            [DataMember]
            public string Timestamp { get; set; }

            [DataMember]
            public Status Status { get; set; }

            [DataMember]
            public Error Error { get; set; }
        }

        [DataContract]
        private class AccountInfo
        {
            [DataMember]
            public string Account { get; set; }

            [DataMember]
            public string Name { get; set; }
        }

        [DataContract]
        private class AccountNameResponse : BaseResponse
        {
            [DataMember]
            public AccountInfo[] Accounts { get; set; }
        }

        [DataContract]
        private class BalanceValues
        {
            [DataMember]
            public decimal Usd { get; set; }

            [DataMember]
            public decimal Euro { get; set; }

            [DataMember]
            public decimal Gold { get; set; }
        }

        [DataContract]
        private class BalanceResponse : BaseResponse
        {
            [DataMember]
            public BalanceValues Balance { get; set; }
        }

        [DataContract]
        private class Transaction
        {
            [DataMember]
            public long Batch { get; set; }

            [DataMember]
            public string Date { get; set; }

            [DataMember]
            public string Reference { get; set; }

            [DataMember]
            public string Type { get; set; }

            [DataMember]
            public string Payer { get; set; }

            [DataMember]
            public string PayerName { get; set; }

            [DataMember]
            public string Payee { get; set; }

            [DataMember]
            public string PayeeName { get; set; }

            [DataMember]
            public Currency Currency { get; set; }

            [DataMember]
            public decimal Amount { get; set; }

            [DataMember]
            public decimal Fee { get; set; }

            [DataMember]
            public decimal Balance { get; set; }

            [DataMember]
            public string Memo { get; set; }

            [DataMember]
            public bool Private { get; set; }

            [DataMember]
            public Source Source { get; set; }
        }

        [DataContract]
        private class TransferResponse : BaseResponse
        {
            [DataMember]
            public Transaction Transaction { get; set; }
        }

        [DataContract]
        private class HistoryResponse : BaseResponse
        {
            [DataMember]
            public bool HasMore { get; set; }

            [DataMember]
            public Transaction[] Transactions { get; set; }
        }

        #endregion

        public JsonApiAgent(ApiCredentials credentials)
            : base(credentials)
        {
            ContentType = "application/json";
        }

        #region IApiAgent Members

        public override Balance Balance()
        {
            BalanceRequest request = InitBaseRequest(new BalanceRequest());
            BalanceResponse response = SendRequest<BalanceRequest, BalanceResponse>("/json/balance", request);

            CheckError(response);
            return new Balance()
            {
                Usd = response.Balance.Usd,
                Euro = response.Balance.Euro,
                Gold = response.Balance.Gold
            };
        }

        public override string AccountName(string accountNumber)
        {
            AccountNameRequest request = InitBaseRequest(new AccountNameRequest()
            {
                Search = accountNumber
            });

            AccountNameResponse response = SendRequest<AccountNameRequest, AccountNameResponse>("/json/accountname", request);
            CheckError(response);
            return response.Accounts.Length == 0 ? null : response.Accounts[0].Name;
        }

        public override HistoryResult History(HistorySpecification specification, int pageIndex, int pageSize)
        {
            HistoryRequest request = InitBaseRequest(new HistoryRequest()
            {
                From = specification.From.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
                Till = specification.Till.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
                Currency = specification.Currency,
                Direction = specification.Direction,
                RelatedAccount = specification.RelatedAccount,
                Reference = specification.Reference,
                Source = specification.Source,
                Private = specification.Private,
                AmountFrom = specification.AmountFrom,
                AmountTo = specification.AmountTo,
                Page = pageIndex,
                Size = pageSize
            });

            List<HistoryItem> resultList = new List<HistoryItem>();
            HistoryResponse response = SendRequest<HistoryRequest, HistoryResponse>("/json/history", request);
            CheckError(response);
            foreach (Transaction transaction in response.Transactions)
            {
                resultList.Add(CreateHistoryItem(transaction));
            }

            return new HistoryResult()
            {
                Items = resultList.ToArray(),
                HasMore = response.HasMore
            };
        }

        public override HistoryItem FindTransaction(long batchNumber)
        {
            FindTransactionRequest request = InitBaseRequest(new FindTransactionRequest()
            {
                Batch = batchNumber
            });

            TransferResponse response = SendRequest<FindTransactionRequest, TransferResponse>("/json/findtransaction", request);
            CheckError(response);
            return response.Transaction == null ? null : CreateHistoryItem(response.Transaction);
        }

        public override HistoryItem Transfer(Transfer transfer)
        {
            TransferRequest request = InitBaseRequest(new TransferRequest()
            {
                Payee = transfer.Payee,
                Amount = transfer.Amount,
                Currency = transfer.Currency,
                Memo = transfer.Memo,
                Reference = transfer.Reference,
                Type = "transfer",
                Private = transfer.Private,
                Purpose = transfer.Purpose
            });

            TransferResponse response = SendRequest<TransferRequest, TransferResponse>("/json/transfer", request);
            CheckError(response);
            return CreateHistoryItem(response.Transaction);
        }

        #endregion

        private TRequest InitBaseRequest<TRequest>(TRequest request)
            where TRequest : BaseRequest
        {
            request.Id = DateTime.UtcNow.Ticks.ToString();
            request.Account = Credentials.AccountNumber;
            request.Api = Credentials.ApiName;
            request.Token = Credentials.CreateSecurityToken();
            return request;
        }

        private static HistoryItem CreateHistoryItem(Transaction transaction)
        {
            return new HistoryItem()
            {
                Amount = transaction.Amount,
                Balance = transaction.Balance,
                Batch = transaction.Batch,
                Currency = transaction.Currency,
                Date = DateTime.ParseExact(transaction.Date, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
                Fee = transaction.Fee,
                Memo = transaction.Memo,
                Payee = transaction.Payee,
                PayeeName = transaction.PayeeName,
                Payer = transaction.Payer,
                PayerName = transaction.PayerName,
                Private = transaction.Private,
                Reference = transaction.Reference,
                Source = transaction.Source
            };
        }

        private static void CheckError(BaseResponse response)
        {
            if (response.Status == Status.Error)
            {
                int errorCode = response.Error.ErrorCode;
                string errorMessage = response.Error.ErrorMessage;
                throw new ApiException(errorMessage)
                {
                    ErrorCode = errorCode
                };
            }
        }

        private TResponse SendRequest<TRequest, TResponse>(string path, TRequest requestData)
        {
            byte[] response;
            using (MemoryStream ms = new MemoryStream())
            {
                using (XmlDictionaryWriter writer = JsonReaderWriterFactory.CreateJsonWriter(ms, Encoding))
                {
                    var requestSerializer = new DataContractJsonSerializer(typeof(TRequest));
                    requestSerializer.WriteObject(writer, requestData);
                    writer.Flush();
                }

                response = SendRequest(path, ms.ToArray());
            }

            using (XmlDictionaryReader reader = JsonReaderWriterFactory.CreateJsonReader(response, XmlDictionaryReaderQuotas.Max))
            {
                var responseSerializer = new DataContractJsonSerializer(typeof(TResponse));

                try
                {
                    return (TResponse)responseSerializer.ReadObject(reader);
                }
                catch (SerializationException sex)
                {
                    throw new ApiException("Response contract is invalid", sex);
                }
            }
        }
    }
}
